package com.partsoft.dits.log;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

import javax.sql.DataSource;

import org.apache.commons.io.IOUtils;
import org.springframework.dao.DataAccessException;

import com.partsoft.dits.EdgeDescriptor;
import com.partsoft.dits.utils.DbUtils;
import com.partsoft.dits.utils.JdbcTemplate;
import com.partsoft.dits.utils.ReturnExecutionCallback;
import com.partsoft.dits.utils.SQLTypeUtils;

import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;


public class BaseDataSourceLogResolver extends AbstractLogResolver {
	
	private DataSource dataSource;
	
	private String namespace;
	
	private String schemaName;
	
	private String logTable = "DITS_LOGS";
	
	private String attachmentTable = "DITS_LOG_ATTS";
	
	public BaseDataSourceLogResolver() {}
	
	public BaseDataSourceLogResolver(DataSource ds) {
		setDataSource(ds);
	}
	
	public BaseDataSourceLogResolver(String namespace, DataSource ds) {
		setNamespace(namespace);
		setDataSource(ds);
	}
	
	public DataSource getDataSource() {
		return dataSource;
	}

	public boolean isImplemented() {
		return getDataSource() != null;
	}

	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}
	
	public String getNamespace() {
		return namespace;
	}

	public void setNamespace(String namespace) {
		this.namespace = namespace;
	}
	
	public String getSchemaName() {
		return schemaName;
	}

	public void setSchemaName(String schemaName) {
		this.schemaName = schemaName;
	}

	public String getLogTable() {
		return logTable;
	}

	public void setLogTable(String logTable) {
		this.logTable = logTable;
	}

	public String getAttatchmentTable() {
		return attachmentTable;
	}

	public void setAttatchmentTable(String attatchmentTable) {
		this.attachmentTable = attatchmentTable;
	}

	@Override
	protected List<LogEntity> resolveLogs(String dclsName, String did, LogType logType, LogLevel logLevel, String queryString, Date beginDay, Date endDay, final int from, final int count) {
		final StringBuffer cSQL = new StringBuffer().append("SELECT * FROM ").append(resolveLogTableName());
		final List<Object> sqlParams = new LinkedList<Object>();
		packagedWhereSQL(getNamespace(), cSQL, sqlParams, dclsName, did, logType, logLevel, queryString, beginDay, endDay);
		cSQL.append(" ORDER BY ").append(resolveLogDateColumn()).append(" DESC");
		final DataSource ds = getDataSource();
		final LogEntityRowMapper logEntityMapper = new LogEntityRowMapper(getDataSource(), 
				resolveAttatchmentTableName(), resolveAttachmentFKColumn(), 
				resolveAttachmentNameColumn(), resolveAttachmentBlobColumn());
		return DbUtils.executeInReadonly(ds, new ReturnExecutionCallback<List<LogEntity>>() {
			public List<LogEntity> callback() {
				JdbcTemplate jdbcHelper = new JdbcTemplate(ds);
				return jdbcHelper.query(cSQL.toString(), sqlParams.toArray(new Object[0]), new ResultSetExtractor<List<LogEntity>>() {
					public List<LogEntity> extractData(ResultSet rs)
							throws SQLException, DataAccessException {
						if (from > 0) try {
							rs.absolute(from);
						} catch (SQLException e) {
							int i = 0; 
							while(++i < from && rs.next());
						}
						int readc = 0;
						List<LogEntity> list = new ArrayList<LogEntity>(count);
						while(rs.next() && (count < 0 || readc < count)) {
							list.add(logEntityMapper.mapRow(rs, readc++));
						}
						return list;
					}
				});
			}
		});
	}
	
	protected void packagedWhereSQL(String namespace, StringBuffer cSQL, List<Object> sqlParams, String dclsName, String did, LogType logType, LogLevel logLevel, String queryString, Date beginDay, Date endDay) {
		Assert.notNull(cSQL);
		Assert.notNull(sqlParams);
		if (StringUtils.hasText(namespace) || (StringUtils.hasText(dclsName) && StringUtils.hasText(did)) || logLevel != null || logType != null || StringUtils.hasText(queryString) || beginDay != null || endDay != null) {
			cSQL.append(" WHERE ");
			boolean appended = false;
			if (StringUtils.hasText(namespace)) {
				cSQL.append(resolveNamespaceColumn()).append(" = ?");
				sqlParams.add(SQLTypeUtils.toSqlParameterValue(namespace, Types.VARCHAR));
				appended = true;
			}
			if (StringUtils.hasText(dclsName) && StringUtils.hasText(did)) {
				if (appended) cSQL.append(" AND ");
				cSQL.append(resolveDescriptorClassColumn()).append(" = ? AND ");
				cSQL.append(resolveDescriptorIDColumn()).append(" = ?");
				sqlParams.add(SQLTypeUtils.toSqlParameterValue(dclsName, Types.VARCHAR));
				sqlParams.add(SQLTypeUtils.toSqlParameterValue(did, Types.VARCHAR));
				appended = true;
			}
			if (logLevel != null) {
				if (appended) cSQL.append(" AND ");
				cSQL.append(resolveLogLevelColumn()).append(" = ?");
				sqlParams.add(SQLTypeUtils.toSqlParameterValue(logLevel.getCode(), Types.INTEGER));
				appended = true;
			}
			if (logType != null) {
				if (appended) cSQL.append(" AND ");
				cSQL.append(resolveLogTypeColumn()).append(" = ?");
				sqlParams.add(SQLTypeUtils.toSqlParameterValue(logType.getCode(), Types.INTEGER));
				appended = true;
			}
			if (StringUtils.hasText(queryString)) {
				if (appended) cSQL.append(" AND ");
				cSQL.append(resolveLogContentColumn()).append(" like ?");
				sqlParams.add(SQLTypeUtils.toSqlParameterValue("%"+queryString+"%", Types.VARCHAR));
				appended = true;
			}
			
			if (beginDay != null && endDay != null) {
				if (appended) cSQL.append(" AND ");
				cSQL.append("(").append(resolveLogDateColumn()).append(" >= ? AND ").append(resolveLogDateColumn()).append(" <= ?)");
				sqlParams.add(SQLTypeUtils.toSqlParameterValue(beginDay, Types.DATE));
				sqlParams.add(SQLTypeUtils.toSqlParameterValue(endDay, Types.DATE));
				appended = true;
			} else if (beginDay != null) {
				if (appended) cSQL.append(" AND ");
				cSQL.append(resolveLogDateColumn()).append("< ?");
				sqlParams.add(SQLTypeUtils.toSqlParameterValue(beginDay, Types.TIMESTAMP));
				appended = true;
			} else if (endDay != null) {
				if (appended) cSQL.append(" AND ");
				cSQL.append(resolveLogDateColumn()).append("> ?");
				sqlParams.add(SQLTypeUtils.toSqlParameterValue(endDay, Types.TIMESTAMP));
				appended = true;
			}
		}
	}

	@Override
	protected int resolveLogCount(String dclsName, String did, LogType logType, LogLevel logLevel, String queryString, Date beginDay, Date endDay) {
		final StringBuffer cSQL = new StringBuffer().append("SELECT COUNT(").append(resolveLogIDColumn()).append(") FROM ").append(resolveLogTableName());
		final List<Object> sqlParams = new LinkedList<Object>();
		packagedWhereSQL(getNamespace(), cSQL, sqlParams, dclsName, did, logType, logLevel, queryString, beginDay, endDay);
		final DataSource ds = getDataSource();
		return DbUtils.executeInReadonly(ds, new ReturnExecutionCallback<Integer>() {
			public Integer callback() {
				JdbcTemplate jdbcHelper = new JdbcTemplate(ds);
				return jdbcHelper.queryForInt(cSQL.toString(), sqlParams.toArray(new Object[0]));
			}
		});
	}
	
	
	
	protected String resolveLogIDColumn() {
		return "SID";
	}
	
	protected String resolveLogDateColumn() {
		return "LOGDATE";
	}
	
	protected String resolveLogTableName() {
		return DbUtils.packSchemaTableName(getSchemaName(), getLogTable());
	}
	
	protected String resolveNamespaceColumn() {
		return "SNAME";
	}
	
	protected String resolveDescriptorClassColumn() {
		return "DCLASSNAME";
	}
	
	protected String resolveDescriptorIDColumn() {
		return "DCLASSID";
	}
	
	protected String resolveLogLevelColumn() {
		return "LOGLEVEL";
	}
	
	protected String resolveLogTypeColumn() {
		return "LOGTYPE";
	}
	
	protected String resolveLogContentColumn() {
		return "CMESSAGE";
	}
	
	protected String resolveAttatchmentTableName() {
		return DbUtils.packSchemaTableName(getSchemaName(), getAttatchmentTable());
	}
	
	protected String resolveAttachmentFKColumn() {
		return "LOGID";
	}
	
	protected String resolveAttachmentNameColumn() {
		return "CNAME";
	}
	
	protected String resolveAttachmentBlobColumn() {
		return "OATT";
	}
	
	public static class LogEntityRowMapper implements RowMapper<LogEntity> {
		
		private final DataSource dataSource;
		
		private final String attachmentTable;
		
		private final String attachmentNameColumn;
		
		private final String attachmentFKColumn;
		
		private final String attachmentBlobColumn;
		
		public LogEntityRowMapper(DataSource dataSource, String attachmentTable, String attachmentFKColumn, String attachmentNameColumn, String attachmentBlobColumn) {
			this.dataSource = dataSource;
			this.attachmentTable = attachmentTable;
			this.attachmentNameColumn = attachmentNameColumn;
			this.attachmentFKColumn = attachmentFKColumn;
			this.attachmentBlobColumn = attachmentBlobColumn;
			Assert.notNull(this.dataSource);
			Assert.hasText(this.attachmentTable);
			Assert.hasText(this.attachmentNameColumn);
			Assert.hasText(this.attachmentFKColumn);
		}

		@SuppressWarnings("unchecked")
		public LogEntity mapRow(ResultSet rs, int rowNum) throws SQLException {
			BaseLogEntity log = new BaseLogEntity();
			log.setId(rs.getString("SID"));
			log.setContent(rs.getString("CMESSAGE"));
			String dcs = rs.getString("DCLASSNAME");
			String dcsid = rs.getString("DCLASSID");
			if (StringUtils.hasText(dcs) && StringUtils.hasText(dcsid)) {
				try {
					log.setDescriptorClass((Class<? extends EdgeDescriptor>) ClassUtils.forName(dcs, Thread.currentThread().getContextClassLoader()));
					log.setDescriptorIdentity(dcsid);
				} catch (Exception e) {}
			}
			log.setClassName(rs.getString("CNAME"));
			log.setFileName(rs.getString("FNAME"));
			log.setMethodName(rs.getString("MNAME"));
			log.setLineNumber(rs.getString("FLINE"));
			log.setThreadName(rs.getString("TNAME"));
			log.setLevel(LogLevel.fromCode(rs.getInt("LOGLEVEL")));
			log.setType(LogType.fromCode(rs.getInt("LOGTYPE")));
			log.setLogDate(rs.getTimestamp("LOGDATE"));
			log.setAttachments(new LogAttachmentList(dataSource, attachmentTable, attachmentFKColumn, attachmentNameColumn, attachmentBlobColumn, log.getId()));
			//加载附件
			return log;
		}
	}
	
	public static class LogAttachmentList implements List<LogAttachment> {
		
		private final DataSource dataSource;
		
		private final String logId;
		
		private final String attachmentTable;
		
		private final String attachmentNameColumn;
		
		private final String attachmentFKColumn;
		
		private final String attachmentBlobColumn;
		
		private List<LogAttachment> atts;
		
		private boolean attachmentsLoaded = false;
		
		
		public LogAttachmentList(DataSource dataSource, String attachmentTable, String attachmentFKColumn, String attachmentNameColumn, String attachmentBlobColumn, String logId) {
			this.dataSource = dataSource;
			this.logId = logId;
			this.attachmentTable = attachmentTable;
			this.attachmentNameColumn = attachmentNameColumn;
			this.attachmentFKColumn = attachmentFKColumn;
			this.attachmentBlobColumn = attachmentBlobColumn;
			Assert.notNull(this.dataSource);
			Assert.hasText(this.logId);
			Assert.hasText(this.attachmentTable);
			Assert.hasText(this.attachmentNameColumn);
			Assert.hasText(this.attachmentFKColumn);
			Assert.hasText(this.attachmentBlobColumn);
		}
		
		private synchronized void loadAttachments() {
			if (attachmentsLoaded) return;
			final String tSQL = String.format("SELECT %s FROM %s WHERE %s = ?", attachmentNameColumn, attachmentTable, attachmentFKColumn);
			final Object []params = new Object[]{
					SQLTypeUtils.toSqlParameterValue(logId, Types.VARCHAR)
			};
			atts = new ArrayList<LogAttachment>(DbUtils.executeInReadonly(dataSource, new ReturnExecutionCallback<List<LogAttachment>>() {
				public List<LogAttachment> callback() {
					JdbcTemplate jdbcHelper = new JdbcTemplate(dataSource);
					return jdbcHelper.query(tSQL.toString(), params, new RowMapper<LogAttachment>(){
						public LogAttachment mapRow(ResultSet rs, int rowNum) throws SQLException {
							return new DataSourceLogAttachment(rs.getString(attachmentNameColumn), dataSource, attachmentTable, attachmentFKColumn, attachmentNameColumn, attachmentBlobColumn, logId);
						}
					});
				}
			}));
			attachmentsLoaded = true;
		}

		public boolean add(LogAttachment e) {
			loadAttachments();
			return atts.add(e);
		}

		public void add(int index, LogAttachment element) {
			loadAttachments();
			atts.add(index, element);
		}

		public boolean addAll(Collection<? extends LogAttachment> c) {
			loadAttachments();
			return atts.addAll(c);
		}

		public boolean addAll(int index, Collection<? extends LogAttachment> c) {
			loadAttachments();
			return atts.addAll(index, c);
		}

		public boolean contains(Object obj) {
			loadAttachments();
			return atts.contains(obj);
		}

		public boolean remove(Object obj) {
			loadAttachments();
			return atts.remove(obj);
		}

		public void clear() {
			loadAttachments();
			atts.clear();
		}

		public boolean containsAll(Collection<?> c) {
			loadAttachments();
			return atts.containsAll(c);
		}

		public boolean isEmpty() {
			loadAttachments();
			return atts.isEmpty();
		}

		public Iterator<LogAttachment> iterator() {
			loadAttachments();
			return atts.iterator();
		}

		public LogAttachment get(int i) {
			loadAttachments();
			return atts.get(i);
		}

		public LogAttachment remove(int i) {
			loadAttachments();
			return atts.remove(i);
		}

		public int indexOf(Object obj) {
			loadAttachments();
			return atts.indexOf(obj);
		}

		public int lastIndexOf(Object obj) {
			loadAttachments();
			return atts.lastIndexOf(obj);
		}

		public ListIterator<LogAttachment> listIterator() {
			loadAttachments();
			return atts.listIterator();
		}

		public ListIterator<LogAttachment> listIterator(int i) {
			loadAttachments();
			return atts.listIterator(i);
		}

		public boolean removeAll(Collection<?> c) {
			loadAttachments();
			return atts.removeAll(c);
		}

		public boolean retainAll(Collection<?> c) {
			loadAttachments();
			return atts.retainAll(c);
		}

		public LogAttachment set(int index, LogAttachment element) {
			loadAttachments();
			return atts.set(index, element);
		}

		public int size() {
			loadAttachments();
			return atts.size();
		}

		public Object[] toArray() {
			loadAttachments();
			return atts.toArray();
		}

		public List<LogAttachment> subList(int i, int j) {
			loadAttachments();
			return atts.subList(i, j);
		}

		public <T> T[] toArray(T[] a) {
			loadAttachments();
			return atts.toArray(a);
		}
	}
	
	public static class DataSourceLogAttachment extends AbstractLogAttachment implements LogAttachment {
		
		private static final long serialVersionUID = -857120344771508726L;

		private byte []bytes;
		
		private boolean byteLoaded = false;
		
		private transient String logId;
		
		private transient final DataSource dataSource;
		
		private transient final String attachmentTable;
		
		private transient final String attachmentNameColumn;
		
		private transient final String attachmentFKColumn;
		
		private transient final String attachmentBlobColumn;

		
		public DataSourceLogAttachment(String attName, DataSource dataSource, String attachmentTable, String attachmentFKColumn, String attachmentNameColumn, String attachmentBlobColumn, String logId) {
			super(attName);
			this.dataSource = dataSource;
			this.logId = logId;
			this.attachmentTable = attachmentTable;
			this.attachmentNameColumn = attachmentNameColumn;
			this.attachmentFKColumn = attachmentFKColumn;
			this.attachmentBlobColumn = attachmentBlobColumn;
			Assert.notNull(this.dataSource);
			Assert.hasText(this.logId);
			Assert.hasText(this.attachmentTable);
			Assert.hasText(this.attachmentNameColumn);
			Assert.hasText(this.attachmentFKColumn);
			Assert.hasText(this.attachmentBlobColumn);
		}
		
		public InputStream getInputStream() throws IOException {
			return new ByteArrayInputStream(getBytes());
		}

		public byte[] getBytes() throws IOException {
			loadBytes();
			return bytes;
		}
		
		private synchronized void  loadBytes() {
			if (byteLoaded) return;
			final String tSQL = String.format("SELECT %s FROM %s WHERE %s = ? AND %s = ?", attachmentBlobColumn, attachmentTable, attachmentFKColumn, attachmentNameColumn);
			final Object []params = new Object[]{
					SQLTypeUtils.toSqlParameterValue(logId, Types.VARCHAR),
					SQLTypeUtils.toSqlParameterValue(getName(), Types.VARCHAR)
			};
			bytes =DbUtils.executeInReadonly(dataSource, new ReturnExecutionCallback<byte[]>() {
				public byte[] callback() {
					JdbcTemplate jdbcHelper = new JdbcTemplate(dataSource);
					return jdbcHelper.queryForObject(tSQL, params, new RowMapper<byte[]>() {
						public byte[] mapRow(ResultSet rs, int rowNum) throws SQLException {
							InputStream ism = rs.getAsciiStream(attachmentBlobColumn);
							try {
								return IOUtils.toByteArray(ism);
							} catch (IOException e) {
								throw new IllegalStateException(e.getMessage(), e);
							} finally {
								IOUtils.closeQuietly(ism);
							}
						}
					});
				}
			});
			byteLoaded = true;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((logId == null) ? 0 : logId.hashCode()) + ((getName() == null) ? 0 : getName().hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			DataSourceLogAttachment other = (DataSourceLogAttachment) obj;
			if (logId == null) {
				if (other.logId != null)
					return false;
			} else if (!logId.equals(other.logId))
				return false;
			if (getName() == null) {
				if (other.getName() != null)
					return false;
			} else if (!getName().equals(other.getName()))
				return false;
			return true;
		}
	}

}
